Skip to content

ci: wire-shape contract gate (QF-14) — block Java @JsonProperty drift from OpenAPI#137

Closed
saurabhjain1592 wants to merge 3 commits into
mainfrom
feature/qf-14-wire-shape
Closed

ci: wire-shape contract gate (QF-14) — block Java @JsonProperty drift from OpenAPI#137
saurabhjain1592 wants to merge 3 commits into
mainfrom
feature/qf-14-wire-shape

Conversation

@saurabhjain1592

Copy link
Copy Markdown
Member

Summary

Adds a CI gate that blocks any PR introducing drift between Java @JsonProperty annotations and the OpenAPI specs pinned via tests/fixtures/wire-shape-baseline.json::openapi_specs_sha.

Mirrors the Python (QF-15), Go (QF-11), and TypeScript (QF-12) wire-shape gates already shipped in those SDKs.

Four gates

  1. Cross-spec schema divergence — same schema name declared with different shapes across spec files. 8 baselined.
  2. Intra-file duplicates — same schema name declared twice in one file. PolicyMatch in orchestrator-api.yaml is currently baselined.
  3. Per-type SDK-vs-spec drift — wire-name diff between Java @JsonProperty and the spec, baseline-aware. 39 baselined.
  4. Registered-type rename-escape — types in the baseline that disappear from either side fail the gate.

The pinned spec SHA itself is guarded by a spec-pin-bump PR label so a single PR can't both move the SHA and silence drift.

Initial baseline at SHA bf1ca22

  • 70 registered Java↔OpenAPI type pairs (more than 2× a naive outer-class-only port)
  • 39 per-type drift entries — burndown follow-ups
  • 8 cross-spec divergences — platform-side reconciliation tracked separately
  • 1 intra-file duplicate (PolicyMatch)

Why brace-depth parsing

WorkflowTypes.java alone holds 10+ wire types nested inside an outer namespace class, plus 6 inner enums with @JsonProperty on their values. A naive regex that captures only the outermost class per file silently attributes 91 mixed annotations to WorkflowTypes, missing genuine wire types like CreateWorkflowRequest, StepGateRequest, RetryContext, StepGateResponse, WorkflowStatus, GateDecision, MediaGovernanceConfig. Source discovery walks brace depth on string- and comment-stripped text so annotations land on the innermost enclosing class/record/interface/enum.

Re-baselining

python3 scripts/wire_shape/refresh.py /path/to/getaxonflow/axonflow/docs/api

Atomic write (temp + rename) so a mid-encode crash can't poison the baseline.

Test plan

  • Validator passes on freshly generated baseline (positive case)
  • Validator fails when a @JsonProperty is renamed to introduce drift (negative case across nested types: RetryContext, StepGateRequest, MarkStepCompletedRequest)
  • Validator skips with exit 0 when AXONFLOW_OPENAPI_SPECS_DIR is unset / not a directory
  • Validator fails with exit 1 when specs dir is empty or schema-less
  • Brace parser correctly attributes annotations on nested classes (WorkflowTypes.CreateWorkflowRequest) and inner enums (WorkflowStatus, GateDecision)
  • spec-pin-bump label exists on this repo (created in this PR)
  • CI run on this PR is green

Adds a CI gate that fails any PR introducing drift between Java
@JsonProperty annotations and the OpenAPI specs pinned at
tests/fixtures/wire-shape-baseline.json::openapi_specs_sha.

Four gates:

1. Cross-spec schema divergence — same schema name declared with
   different shapes across spec files.
2. Intra-file duplicates — same schema name declared twice in one
   spec file (PolicyMatch in orchestrator-api.yaml is the existing
   example, baselined for now).
3. Per-type SDK-vs-spec drift — wire field names diff between
   Java @JsonProperty and the spec, baseline-aware.
4. Registered-type rename-escape — types in the baseline that
   disappear from either side fail the gate.

The pinned spec SHA is itself guarded by a `spec-pin-bump` PR label
so a single PR can't both move the SHA and silence drift.

Source discovery walks brace depth on string- and comment-stripped
text so annotations are attributed to the innermost enclosing
class/record/interface/enum rather than the file's outer class.
Without this, types like WorkflowTypes.CreateWorkflowRequest and
WorkflowTypes.RetryContext (10+ wire types nested inside the
WorkflowTypes namespace class) would silently escape coverage.

Initial baseline at SHA bf1ca22:
- 70 registered Java<->OpenAPI type pairs
- 39 per-type drift entries (burndown follow-ups)
- 8 cross-spec divergences (platform-side reconciliation tracked
  separately)
- 1 intra-file duplicate (PolicyMatch)

Mirrors the Python, Go, and TypeScript wire-shape gates.

Re-baseline:
  python3 scripts/wire_shape/refresh.py /path/to/community/docs/api
- load_baseline raises SystemExit with regen hint when the JSON file
  is malformed instead of dumping an opaque traceback.
- write_baseline cleans up its tmp sidecar on any exception so a
  crashed run can't poison the next refresh from the same PID.
- The validator now exits 1 (not 0) when AXONFLOW_OPENAPI_SPECS_DIR
  is set but doesn't point at a directory. A misconfigured CI step
  silently disabling the gate produces a green check on a non-running
  validator, which we refuse to do. An unset env still skips with 0.
- Workflow's SHA-bump guard distinguishes "baseline missing on base
  branch" (genuine first-pin introduction) from "baseline present
  but unparseable" (bypass attempt). A malformed baseline on the
  base branch can no longer route a labelless PR through the
  first-pin path.
- Java source parser:
    * Recognises Java 15+ text blocks (`"""..."""`). Previously the
      first `"""` looked like quote-empty-quote and the body parsed
      as live source — fake `@JsonProperty(...)` and `class X {`
      inside a text block silently corrupted attribution.
    * Filters annotation matches whose position falls inside a
      blanked range (string, comment, text block) so the wire-name
      regex on raw content can't grab annotations the structural
      pass already nullified.
    * Attributes record-parameter annotations to the record. Pending
      decls are now tracked between their token and the body `{`,
      so `record Foo(@JsonProperty("x") int x) {}` no longer drops
      the wire name. The SDK has no records today; this is forward
      defence for the next type-class refactor.
- Documents the remaining anonymous-inner-class limitation (no
  `class` keyword, so annotations would leak to the enclosing type).
  The SDK does not put @JsonProperty inside anonymous inners.

Synthetic-fixture tests cover record params, text blocks with fake
annotations, line and block comments, nested classes, and enum
values. Real Java SDK baseline unchanged: 70 registered types, 39
drift, 8 cross-spec, 1 intra-file.
Gate 1 (cross-spec divergence) only iterated currently observed
schemas. A baselined divergence that the platform has since
reconciled silently lingered in the baseline forever; the same old
incompatible shape could be reintroduced and pass the gate because
its fingerprint matched a stale entry that should have been deleted.

Adds the reverse pass that Gate 2 already does for intra-file
duplicates: any baselined cross-spec name that is no longer
observed in the current specs fails the run with a pointer at the
specific baseline key to delete.

Verified locally:
- Positive run on clean baseline still exits 0.
- Adding a phantom 'PhantomDivergence' entry to baseline.cross_spec_
  duplicates causes the validator to exit 1 with a clear "remove
  from baseline" message naming the stale key.
@saurabhjain1592

Copy link
Copy Markdown
Member Author

Closing as superseded.

The wire-shape contract gate this PR was opening landed on main via the parallel sweep PR #138 (which bundled the gate with the type alignment work). The CONTRIBUTING.md scaffold landed via #139 (burndown policy). Re-merging this branch would:

The QF-14 work is shipped — verifying with git ls-tree origin/main scripts/wire_shape/ tests/fixtures/wire-shape-baseline.json shows all the gate files in place; the Java [Unreleased] CHANGELOG already credits the gate landing.

@saurabhjain1592 saurabhjain1592 deleted the feature/qf-14-wire-shape branch April 25, 2026 13:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant